import datetime
import functools
import logging
import signal
import os
import math
import time

from enum import Enum
from collections import deque
from threading import Timer

from absl import app, flags
from tornado.ioloop import PeriodicCallback

from coin.base.datetime_util import to_datetime
from coin.base.timestamp import get_timestamp
from coin.base.param_util import to_list
from coin.exchange.base.order_gateway_config import convert_og_config_from_dict
from coin.exchange.base.strategy_logger import StrategyLogProvider
from coin.exchange.kr_rest.product.product_impl import (generate_product_from_subreq)
from coin.exchange.kr_rest.product_holders import get_holder_from_product
from coin.exchange.util.feed_checker import get_default_feed_checker_config
from coin.proto.coin_feed_pb2 import BestBookEntry, BookEntry
from coin.proto.coin_request_pb2 import AccountRequestProto
from coin.proto.coin_telemetry_pb2 import LatencyProto

from coin.strategy.accounting.pnl_balance_printer.util import (get_quote_currency, reserve_as_proto)
from coin.strategy.executor.aggressive.util import (convert_executor_config_from_dict)
from coin.strategy.executor.aggressive.factory import construct_agg_executor
from coin.strategy.executor.util.cancel_all_executor import (SimpleCancelAllExecutor)
from coin.strategy.hk_hard_arb.index_pricer import IndexPricer
from coin.strategy.hk_hard_arb.pricer import PositionLeanPricer
from coin.strategy.hk_hard_arb.util import load_clean_json
from coin.strategy.mm.base.bookmap import BookMap
from coin.strategy.mm.base.unary_multifeed_strategy_base import UnaryMultifeedStrategyBase

from coin.strategy.mm.subscription import (
    FeedSubscriptionRequest,
    SubscriptionRequest,
)
from coin.strategy.tool.fill_log_dumper import FillLogDumper
from coin.strategy.hk_hard_arb.amplitude_checker import AmplitudeChecker
from coin.strategy.accounting.pnl_balance_printer.factory import get_pnl_balance_printer_cls
from coin.strategy.util.request_util import convert_strategy_request_from_dict
from coin.support.telemetry.latency_recorder import LatencyRecorder, LifeOfSignal
from coin.strategy.hk_hard_arb.unhedged_mm_3 import (
    PassUnhedgeStrategyConfig,
)
from coin.exchange.base.order_gateway import OrderSide
from coin.exchange.base.kr_rest.private_key import get_private_key

from coin.strategy.dex_strat.simple_amm.amm_utils import (WindowEstimator, BiasCalculator, SimpleBookEntry)

FLAGS = flags.FLAGS

class SingleProductSubStrategyAMMBase(object):
  def __init__(self, model_profile, dry_run, logger, strategy_logger, latency_recorder, parent):
    self._dry_run = dry_run
    self._logger = logger
    self._strategy_logger = strategy_logger
    self._latency_recorder = latency_recorder
    self._parent = parent
    config = model_profile.config_dict
    self._config = config
    self._trade_subreq = model_profile.trade_subreq
    self._trade_product = model_profile.trade_product
    self._ref_subreq = model_profile.ref_subreq
    self._ref_product = model_profile.ref_product
    assert len(self._ref_subreq) == len(self._ref_product)

    self._edge = None
    self._spread = None
    self._spread_hard = None
    self._pull_edge = None

    self._bbo_protection = config.get('bbo_protection', False)
    self._edge = config['edge_bp'] / 10000.

    products = self._ref_product + to_list(self._trade_product)
    self._bookmap = BookMap(products, logger=self._logger)

    tick = get_holder_from_product(self._trade_product).product_info.first_tick_size
    self._pricer = IndexPricer(self._config, tick)
    self._pos_lean_pricer = PositionLeanPricer(min_pos=config['min_pos'],
                                               max_pos=config['max_pos'],
                                               lot_size=config['lot_size'],
                                               lean_edge_bp=config.get('lean_edge_bp', 0))

    self._last_book_write_ts = get_timestamp()
    self._last_binance_book_ts = 0
    self._last_uniswap_book_ts = 0
    self._last_binance_ask0 = 0.0
    self._last_binance_bid0 = 0.0
    self._last_uniswap_ask0 = 0.0
    self._last_uniswap_bid0 = 0.0
    self._kmax_diff_ms = int(self._config.get('max_diff_ms', 500))

    self._kgas_fee = float(self._config.get('gas_fee', 0.5))
    self.reserve_pos = float(self._config.get('reserve', 0.5))
    self.max_pos = float(self._config.get('max_pos', 0.9))
    self.min_pos = float(self._config.get('min_pos', 0.1))
    self.current_pos = self.reserve_pos
    self.lot_size = float(self._config.get('lot_size', 0.1))

    self._executor = None
    self._exit = False
    self._is_buy_submitted = False
    self._is_sell_submitted = False

  def set_exit(self):
    self._exit = True

  def prepare_base(self, feed_subsystem):
    assert feed_subsystem is not None
    product = self._trade_product
    subreq = self._trade_subreq
    trade_feed_sub_request = FeedSubscriptionRequest.create(to_list(product), subreq)
    feed_subsystem.subscribe_book(trade_feed_sub_request, functools.partial(
        self.on_book, self._trade_product, self._trade_subreq))
    for product, sub_req in zip(self._ref_product, self._ref_subreq):
      feed_sub_request = FeedSubscriptionRequest.create(to_list(product), sub_req)
      feed_subsystem.subscribe_book(
          feed_sub_request, functools.partial(self.on_book, product, sub_req))

    feed_subsystem.subscribe_trade(trade_feed_sub_request, functools.partial(
        self.on_trade, self._trade_product, self._trade_subreq))
    for product, sub_req in zip(self._ref_product, self._ref_subreq):
      feed_sub_request = FeedSubscriptionRequest.create(to_list(product), sub_req)
      feed_subsystem.subscribe_trade(
          feed_sub_request, functools.partial(self.on_trade, product, sub_req))
    return True

  def on_book_base(self, product, sub_req, book):
    if self._exit:
      return False

    if product == self._trade_product and self._executor is not None:
      book_ts = book.timestamp
      if book_ts - self._last_book_write_ts >= 5 * 10**9:
        self._last_book_write_ts = book_ts
        best_book_entry = BestBookEntry(symbol=product.symbol,
                                        fetched_ts=book_ts,
                                        ask0=BookEntry(price_float=book.ask0().price),
                                        bid0=BookEntry(price_float=book.bid0().price))
        self._strategy_logger.write_best_book_entry(
            best_book_entry, account_request=self._executor.og.og_logger.request)

    if self._latency_recorder is not None:
      account_request = AccountRequestProto(market_type=sub_req.market_type,
                                            exchange=sub_req.exchange,
                                            api_version=sub_req.api_version)
      self._latency_recorder.begin(LatencyProto.FEED_LATENCY)
      self._latency_recorder.set_time(LifeOfSignal.RAW_FEED_PARSE_STARTED,
                                      account_request=account_request,
                                      timestamp=book.timestamp)
      self._latency_recorder.set_time(
          LifeOfSignal.ON_BOOK_FEED,
          account_request=account_request,
      )
      self._latency_recorder.end()
      self._latency_recorder.begin()
      self._latency_recorder.set_time(LifeOfSignal.RAW_FEED_PARSE_STARTED,
                                      account_request=account_request,
                                      timestamp=book.timestamp)
      self._latency_recorder.set_time(
          LifeOfSignal.ON_BOOK_FEED,
          account_request=account_request,
      )

    self._bookmap.set_book(product, book)

    # Check book freshness and sanity
    if not self._bookmap.is_book_ready() or not self._bookmap.is_book_sane():
      return False

    ref_book = [self._bookmap.get_book(product) for product in self._ref_product]
    trade_book = self._bookmap.get_book(self._trade_product)
    self._pricer.update(ref_book, trade_book)

    if product.exchange == 'Uniswap':
      if book.timestamp - self._last_uniswap_book_ts >= 200 * 10 ** 6:
        self._last_uniswap_book_ts = book.timestamp
        self._last_uniswap_ask0 = book.ask0().price
        self._last_uniswap_bid0 = book.bid0().price
        self._logger.info('%s %s Book product: %s, askp: %s, bidp: %s' % (
          to_datetime(book.timestamp), book.timestamp, product.symbol, book.ask0().price, book.bid0().price))
      else:
        return False
    elif product.exchange == 'Binance':
      if book.timestamp - self._last_binance_book_ts >= 500 * 10 ** 6:
        self._last_binance_book_ts = book.timestamp
        self._last_binance_ask0 = book.ask0().price
        self._last_binance_bid0 = book.bid0().price
        self._logger.info('%s %s Book product: %s, askp: %s, bidp: %s' % (
          to_datetime(book.timestamp), book.timestamp, product.symbol, book.ask0().price, book.bid0().price))
      else:
        return False
    if self._executor is None or not self._pricer.ready:
      return False
    return True

  def on_trade_base(self, product, sub_req, trade):
    if product.exchange == 'Uniswap':
      self._logger.info('%s Trade product: %s, price: %s, qty: %s, side: %s' % (
        to_datetime(trade.timestamp), product.symbol, trade.price, trade.qty, trade.side))
    return True

  def on_og_reset(self, *, order_gateway):
    self._logger.info('Intializing executor...')
    executor_params = convert_executor_config_from_dict(self._trade_product.symbol, self._config)
    self._executor = construct_agg_executor(self._trade_subreq,
                                            executor_params,
                                            order_gateway,
                                            self._logger,
                                            latency_recorder=self._latency_recorder)
    self._strategy_logger.write_executor_config(executor_params)

  def prepare_process(self, feed_subsystem):
    return

  def on_book_process(self, product, sub_req, book):
    return

  def on_trade_process(elf, product, sub_req, trade):
    return

  def prepare(self, feed_subsystem):
    if not self.prepare_base(feed_subsystem):
      return
    self.prepare_process(feed_subsystem)

  def on_book(self, product, sub_req, book):
    if not self.on_book_base(product, sub_req, book):
      return
    self.on_book_process(product, sub_req, book)

  def on_trade(self, product, sub_req, trade):
    if not self.on_trade_base(product, sub_req, trade):
      return
    self.on_trade_process(product, sub_req, trade)

class SingleProductSubStrategyAMMGeneralBase(object):
  def __init__(self, model_profile, dry_run, logger, strategy_logger, latency_recorder, parent):
    self._dry_run = dry_run
    self._logger = logger
    self._strategy_logger = strategy_logger
    self._latency_recorder = latency_recorder
    self._parent = parent
    config = model_profile.config_dict
    self._config = config
    self._trade_subreq = model_profile.trade_subreq
    self._trade_product = model_profile.trade_product
    self._ref_subreq = model_profile.ref_subreq
    self._ref_product = model_profile.ref_product
    assert len(self._ref_subreq) == len(self._ref_product)

    self._edge = None
    self._spread = None
    self._spread_hard = None
    self._pull_edge = None

    self._bbo_protection = config.get('bbo_protection', False)
    self._edge = config['edge_bp'] / 10000.

    products = self._ref_product + to_list(self._trade_product)
    self._bookmap = BookMap(products, logger=self._logger)

    tick = get_holder_from_product(self._trade_product).product_info.first_tick_size
    self._pricer = IndexPricer(self._config, tick)
    self._pos_lean_pricer = PositionLeanPricer(min_pos=config['min_pos'],
                                               max_pos=config['max_pos'],
                                               lot_size=config['lot_size'],
                                               lean_edge_bp=config.get('lean_edge_bp', 0))

    self._last_book_write_ts = get_timestamp()
    self._last_binance_book_ts = 0
    self._last_uniswap_book_ts = 0
    self._last_binance_ask0 = 0.0
    self._last_binance_bid0 = 0.0
    self._last_uniswap_ask0 = 0.0
    self._last_uniswap_bid0 = 0.0
    self._kmax_diff_ms = int(self._config.get('max_diff_ms', 500))

    self._kgas_fee = float(self._config.get('gas_fee', 0.5))
    self.reserve_pos = float(self._config.get('reserve', 0.5))
    self.max_pos = float(self._config.get('max_pos', 0.9))
    self.min_pos = float(self._config.get('min_pos', 0.1))
    self.current_pos = self.reserve_pos
    self.lot_size = float(self._config.get('lot_size', 0.1))

    self._executor = None
    self._exit = False
    self._is_buy_submitted = False
    self._is_sell_submitted = False

    self._last_book_dict = {}
    for product in products:
      self._last_book_dict[product.exchange] = {}
      self._last_book_dict[product.exchange][product.symbol] = None

  def set_exit(self):
    self._exit = True

  def prepare_base(self, feed_subsystem):
    assert feed_subsystem is not None
    product = self._trade_product
    subreq = self._trade_subreq
    trade_feed_sub_request = FeedSubscriptionRequest.create(to_list(product), subreq)
    feed_subsystem.subscribe_book(trade_feed_sub_request, functools.partial(
        self.on_book, self._trade_product, self._trade_subreq))
    for product, sub_req in zip(self._ref_product, self._ref_subreq):
      feed_sub_request = FeedSubscriptionRequest.create(to_list(product), sub_req)
      feed_subsystem.subscribe_book(
          feed_sub_request, functools.partial(self.on_book, product, sub_req))

    feed_subsystem.subscribe_trade(trade_feed_sub_request, functools.partial(
        self.on_trade, self._trade_product, self._trade_subreq))
    for product, sub_req in zip(self._ref_product, self._ref_subreq):
      feed_sub_request = FeedSubscriptionRequest.create(to_list(product), sub_req)
      feed_subsystem.subscribe_trade(
          feed_sub_request, functools.partial(self.on_trade, product, sub_req))
    return True

  def on_book_base(self, product, sub_req, book):
    if self._exit:
      return False

    if product == self._trade_product and self._executor is not None:
      book_ts = book.timestamp
      if book_ts - self._last_book_write_ts >= 5 * 10**9:
        self._last_book_write_ts = book_ts
        best_book_entry = BestBookEntry(symbol=product.symbol,
                                        fetched_ts=book_ts,
                                        ask0=BookEntry(price_float=book.ask0().price),
                                        bid0=BookEntry(price_float=book.bid0().price))
        self._strategy_logger.write_best_book_entry(
            best_book_entry, account_request=self._executor.og.og_logger.request)

    if self._latency_recorder is not None:
      account_request = AccountRequestProto(market_type=sub_req.market_type,
                                            exchange=sub_req.exchange,
                                            api_version=sub_req.api_version)
      self._latency_recorder.begin(LatencyProto.FEED_LATENCY)
      self._latency_recorder.set_time(LifeOfSignal.RAW_FEED_PARSE_STARTED,
                                      account_request=account_request,
                                      timestamp=book.timestamp)
      self._latency_recorder.set_time(
          LifeOfSignal.ON_BOOK_FEED,
          account_request=account_request,
      )
      self._latency_recorder.end()
      self._latency_recorder.begin()
      self._latency_recorder.set_time(LifeOfSignal.RAW_FEED_PARSE_STARTED,
                                      account_request=account_request,
                                      timestamp=book.timestamp)
      self._latency_recorder.set_time(
          LifeOfSignal.ON_BOOK_FEED,
          account_request=account_request,
      )

    self._bookmap.set_book(product, book)

    # Check book freshness and sanity
    if not self._bookmap.is_book_ready() or not self._bookmap.is_book_sane():
      return False

    ref_book = [self._bookmap.get_book(product) for product in self._ref_product]
    trade_book = self._bookmap.get_book(self._trade_product)
    #self._pricer.update(ref_book, trade_book)

    if self._last_book_dict[product.exchange].get(product.symbol) is None \
    or book.timestamp - self._last_book_dict[product.exchange][product.symbol].timestamp >= 500 * 10 ** 6:
      self._last_book_dict[product.exchange][product.symbol] = SimpleBookEntry(book.timestamp, product.exchange, product.symbol, book.ask0().price, book.bid0().price)
      self._logger.info('%s Book product:%s,%s, askp: %s, bidp: %s' % (
            to_datetime(book.timestamp), product.exchange, product.symbol, book.ask0().price, book.bid0().price))
    else:
      return False

    if self._executor is None:
      return False
    return True

  def on_trade_base(self, product, sub_req, trade):
    if product.exchange == 'Uniswap':
      self._logger.info('%s Trade product: %s, price: %s, qty: %s, side: %s' % (
        to_datetime(trade.timestamp), product.symbol, trade.price, trade.qty, trade.side))
    return True

  def on_og_reset(self, *, order_gateway):
    self._logger.info('Intializing executor...')
    executor_params = convert_executor_config_from_dict(self._trade_product.symbol, self._config)
    self._executor = construct_agg_executor(self._trade_subreq,
                                            executor_params,
                                            order_gateway,
                                            self._logger,
                                            latency_recorder=self._latency_recorder)
    self._strategy_logger.write_executor_config(executor_params)

  def prepare_process(self, feed_subsystem):
    return

  def on_book_process(self, product, sub_req, book):
    return

  def on_trade_process(elf, product, sub_req, trade):
    return

  def prepare(self, feed_subsystem):
    if not self.prepare_base(feed_subsystem):
      return
    self.prepare_process(feed_subsystem)

  def on_book(self, product, sub_req, book):
    if not self.on_book_base(product, sub_req, book):
      return
    self.on_book_process(product, sub_req, book)

  def on_trade(self, product, sub_req, trade):
    if not self.on_trade_base(product, sub_req, trade):
      return
    self.on_trade_process(product, sub_req, trade)

class SimpleAMMSubStrategy(SingleProductSubStrategyAMMBase):
  def __init__(self, model_profile, dry_run, logger, strategy_logger, latency_recorder, parent):
    super().__init__(model_profile, dry_run, logger, strategy_logger, latency_recorder, parent) 
    self._last_op_ts = 0
    self._kmin_op_interval = int(self._config.get('min_op_interval', 40000000000))

    self._kwindow_estimator_normal_record_cnt = int(self._config.get('window_estimator_normal_record_cnt', 3600))
    self._kwindow_estimator_mid_record_cnt = int(self._config.get('window_estimator_mid_record_cnt', 60))
    self._kwindow_estimator_range_multiplier = float(self._config.get('window_estimator_range_multiplier', 2.0))
    
    self._kwindow_size_multiplier = float(self._config.get('window_size_multiplier', 3.0))

    self._kprovide_signal_bar = float(self._config.get('provide_signal_bar', 0.2 ** 2))
    self._knormal_withdraw_bias_record_cnt = int(self._config.get('normal_withdraw_bias_record_cnt', 60))
    self._kmid_provide_bias_record_cnt = int(self._config.get('mid_provide_bias_record_cnt', 600))
    self._klong_provide_bias_record_cnt = int(self._config.get('long_provide_bias_record_cnt', 3600))

    self._window_estimator = WindowEstimator(max_record_cnt=self._kwindow_estimator_normal_record_cnt, range_multiplier=self._kwindow_estimator_range_multiplier)
    self._mid_win_estimator = WindowEstimator(max_record_cnt=self._kwindow_estimator_mid_record_cnt, range_multiplier=self._kwindow_estimator_range_multiplier)
    self._normal_withdraw_bias_calculator = BiasCalculator(max_record_cnt=self._knormal_withdraw_bias_record_cnt, withdraw_signal_bar=1.2 ** 2, provide_signal_bar=self._kprovide_signal_bar)
    self._mid_provide_bias_calculator = BiasCalculator(max_record_cnt=self._kmid_provide_bias_record_cnt, withdraw_signal_bar=2.5 ** 2, provide_signal_bar=self._kprovide_signal_bar)
    self._long_provide_bias_calculator = BiasCalculator(max_record_cnt=self._klong_provide_bias_record_cnt, withdraw_signal_bar=2.5 ** 2, provide_signal_bar=self._kprovide_signal_bar)

    self._kmin_half_win_size = float(self._config.get('min_half_win_size', 75.0))
    self._kcan_adjust_bar = float(self._config.get('can_adjust_bar', 5.0))
    self._kaccept_loss_factor = float(self._config.get('accept_loss_factor', 0.8))
    self._kminus_bias_bar = float(self._config.get('minus_bias_bar', 1.2 ** 2))
    self._kplus_bias_bar = float(self._config.get('plus_bias_bar', 1.1 ** 2))
    self._kbias_bar_decay_max_size = float(self._config.get('bias_bar_decay_max_size', 30.0))
    self._kavg_price_out_of_range = float(self._config.get('avg_price_out_of_range', 10.0))

    self._kminus_bias_adjust_bar = float(self._config.get('minus_bias_adjust_bar', 1.2))
    self._kplus_bias_adjust_bar = float(self._config.get('plus_bias_adjust_bar', 0.8))

    self._kreturn_pos_after_withdraw = bool(self._config.get('return_pos_after_withdraw', False))
    self._kreturn_pos_diff_ratio = float(self._config.get('return_pos_diff_ratio', 0.05))
    self._kreturn_pos_valid_ratio = float(self._config.get('return_pos_valid_ratio', 0.1))

    self._kstatus_record_file_path = str(self._config.get('status_record_file_path', os.path.expanduser('~/simple_amm_status')))

    self._klp_fee_per_unit = 0.000381944 / 2.0
    self._klp_loss_adjust_factor = (0.26 / 0.001924) / 2.0
    self._has_lp_position = False
    self._cur_lp_enter_price = 0.0
    self._cur_lp_enter_amount = 0.0
    self._cur_lp_enter_base_amount = 0.0
    self._cur_lp_enter_quote_amount = 0.0
    self._cur_lp_price_lower = 0.0
    self._cur_lp_price_upper = 0.0
    self._cur_lp_fee_loss_multiplier = 0.0
    self._cur_lp_in_cnt = 0
    self._cur_lp_out_cnt = 0

    self._cur_loss_per_unit = 0.0
    self._cur_lp_loss = 0.0
    self._total_lp_loss = 0.0

    self._cur_lp_fee = 0.0
    self._total_fee = 0.0
    self._total_net_profit = 0.0
    self._last_lp_out_percentage = 0.0

    self.last_processed_uniswap_ts = 0

    self._uniswap_tick_cnt = 0

    self._pos_initialized = False
    self._is_updating_pos = False
    self._is_returning_pos = False
    self._kdefault_amount = 10000.0
    self._op_provide_price = 0.0
    self._op_provide_price_upper = 0.0
    self._op_provide_price_lower = 0.0
    self._op_provide_amount = 0.0
    self._op_withdraw_price = 0.0

  def reset_lp_data(self):
    self._has_lp_position = False
    self._cur_lp_enter_price = 0.0
    self._cur_lp_enter_amount = 0.0
    self._cur_lp_enter_base_amount = 0.0
    self._cur_lp_enter_quote_amount = 0.0
    self._cur_lp_price_lower = 0.0
    self._cur_lp_price_upper = 0.0
    self._cur_lp_fee_loss_multiplier = 0.0
    self._cur_lp_in_cnt = 0
    self._cur_lp_out_cnt = 0
    self._cur_lp_loss = 0.0
    self._cur_lp_fee = 0.0
    self._cur_loss_per_unit = 0.0

  def print_lp_info(self):
    if not self._has_lp_position:
      return
    
    self._logger.info('current liquidity: enter price:%f, amount: %f, price lower:%f, price upper:%f, record in:%d, record out:%f' % (
            self._cur_lp_enter_price, self._cur_lp_enter_amount, self._cur_lp_price_lower, self._cur_lp_price_upper, self._cur_lp_in_cnt, self._cur_lp_out_cnt))
    self._logger.info('current status: current loss:%f, current fee: %f, total loss:%f, total fee:%f, total net profit:%f' % (
      self._cur_lp_loss, self._cur_lp_fee, self._total_lp_loss, self._total_fee, self._total_net_profit))

  def write_status_to_file(self, cur_price, price_upper, price_lower, amount, cur_fee):
    try:
      with open(self._kstatus_record_file_path, "w") as status_file:
        if not(cur_price is None or price_upper is None or price_lower is None or amount is None or cur_fee is None):
          status_msg = '%f,%f,%f,%f,%f' % (cur_price, price_upper, price_lower, amount, cur_fee)
          status_file.write(status_msg)
    except Exception:
        self._logger.exception('Exception during reading status file.')

  def read_status_from_file(self, default_price):
    cur_price = default_price
    price_upper = cur_price + self._kmin_half_win_size
    price_lower = max(0.01, cur_price - self._kmin_half_win_size)
    amount = self._kdefault_amount
    cur_fee = 0.0
    try:
      with open(self._kstatus_record_file_path, "r") as status_file:
        line = status_file.readline()
        linepart = line.split(',')
        if len(linepart) >= 5:
          cur_price = float(linepart[0])
          price_upper = float(linepart[1])
          price_lower = float(linepart[2])
          amount = float(linepart[3])
          cur_fee = float(linepart[4])
    except Exception:
        self._logger.exception('Exception during reading status file.')
    return (cur_price, price_upper, price_lower, amount, cur_fee)

  def initialize_pos(self, price):
    has_nft_token = self._executor.is_holding_nft_token(symbol=self._trade_product.symbol)
    if has_nft_token and not self._pos_initialized and not self._is_updating_pos:
      self._is_updating_pos = True
      self._logger.info("Has position when strategy starts, updating.")
      (cur_price, price_upper, price_lower, amount, cur_fee) = self.read_status_from_file(price)
      self.update_lp_data_after_provide(cur_price, price_upper, price_lower, amount)
      self._cur_lp_fee = cur_fee
      self._is_updating_pos = False
    self._pos_initialized = True

  def update_pos(self):
    has_nft_token = self._executor.is_holding_nft_token(symbol=self._trade_product.symbol)
    if self._has_lp_position != has_nft_token and not self._is_updating_pos:
      self._is_updating_pos = True
      if self._has_lp_position and self._op_withdraw_price > 0.001:
        self.update_lp_data_after_withdraw(self._op_withdraw_price)
        self.write_status_to_file(None, None, None, None, None)
        return_pos_price = self._op_withdraw_price
        self._op_withdraw_price = 0.0
        if self._kreturn_pos_after_withdraw:
          self.return_pos_after_withdraw(return_pos_price)
      elif not self._has_lp_position and self._op_provide_price > 0.001:
        self.update_lp_data_after_provide(self._op_provide_price, self._op_provide_price_upper, self._op_provide_price_lower, self._op_provide_amount)
        self.write_status_to_file(self._op_provide_price, self._op_provide_price_upper, self._op_provide_price_lower, self._op_provide_amount, 0.0)
        self._op_provide_price = 0.0
        self._op_provide_price_upper = 0.0
        self._op_provide_price_lower = 0.0
        self._op_provide_amount = 0.0
      else:
        self._has_lp_position = not self._has_lp_position
      self._is_updating_pos = False

  def return_pos_after_withdraw(self, price):
    cur_pos = self._executor.query_unbiased_position()
    if cur_pos < self._kreturn_pos_valid_ratio * self.reserve_pos:
      time.sleep(5)
      cur_pos = self._executor.query_unbiased_position()
    self._logger.info("Return pos after withdraw. cur_pos:%f, reserve_pos:%f" % (
        cur_pos, self.reserve_pos
      ))
    if cur_pos < self._kreturn_pos_valid_ratio * self.reserve_pos:
      return
    pos_diff = self.reserve_pos - cur_pos
    if abs(pos_diff) > self._kreturn_pos_diff_ratio * self.reserve_pos and not self._is_returning_pos:
      self._is_returning_pos = True
      self._logger.info("Return pos after withdraw. cur_pos:%f, reserve_pos:%f, diff:%f, price:%f" % (
        cur_pos, self.reserve_pos, pos_diff, price
      ))
      if pos_diff > 0.0:
        result = self._executor.submit_agg_order(
              order_side=OrderSide.BUY,
              order_price=price * 1.01,
              order_qty=abs(pos_diff))
        if not result:
          self._logger.info('Order Failed[BUY]!!')
        else:
          self._logger.info('Order Submitted[BUY]!!')
      else:
        result = self._executor.submit_agg_order(
              order_side=OrderSide.SELL,
              order_price=price * 0.99,
              order_qty=abs(pos_diff))
        if not result:
          self._logger.info('Order Failed[SELL]!!')
        else:
          self._logger.info('Order Submitted[SELL]!!')
      self._is_returning_pos = False

  def update_lp_data_after_withdraw(self, cur_price):
    if not self._has_lp_position:
      return
    
    self._total_lp_loss += self._cur_lp_loss
    self._total_fee += self._cur_lp_fee
    self._total_net_profit = self._total_fee - self._total_lp_loss
    if self._cur_lp_out_cnt + self._cur_lp_in_cnt == 0:
      self._last_lp_out_percentage = -1
    else:
      self._last_lp_out_percentage = (float)(self._cur_lp_out_cnt) / (float)(self._cur_lp_out_cnt + self._cur_lp_in_cnt)
    self._logger.info('withdraw liquidity: enter price:%f, quit price: %f, amount: %f, price lower:%f, price upper:%f, record span:%d, record out percentage:%f' % (
            self._cur_lp_enter_price, cur_price, self._cur_lp_enter_amount, self._cur_lp_price_lower, self._cur_lp_price_upper, self._cur_lp_in_cnt + self._cur_lp_out_cnt, self._last_lp_out_percentage))
    self._logger.info('current status: current loss:%f, current fee: %f, total loss:%f, total fee:%f, total net profit:%f' % (
      self._cur_lp_loss, self._cur_lp_fee, self._total_lp_loss, self._total_fee, self._total_net_profit))
    self.reset_lp_data()

  def update_lp_data_after_provide(self, cur_price, price_upper, price_lower, amount):
    if self._has_lp_position:
      return

    self._has_lp_position = True
    self._cur_lp_enter_price = cur_price
    self._cur_lp_enter_amount = amount
    self._cur_lp_enter_base_amount = amount / (cur_price * 2.0) 
    self._cur_lp_enter_quote_amount = amount / 2.0
    self._cur_lp_price_lower = price_lower
    self._cur_lp_price_upper = price_upper
    self._cur_lp_fee_loss_multiplier = 100.0 / (price_upper - price_lower)
    self._cur_lp_in_cnt = 0
    self._cur_lp_out_cnt = 0
    self._cur_lp_loss = 0.0
    self._cur_lp_fee = 0.0
    self.calculate_current_loss(cur_price + 1.0)
    self._cur_loss_per_unit = self._cur_lp_loss
    self._logger.info('provide liquidity: enter price:%f, amount: %f, price lower:%f, price upper:%f, loss per unit:%f' % (
      self._cur_lp_enter_price, self._cur_lp_enter_amount, self._cur_lp_price_lower, self._cur_lp_price_upper, self._cur_loss_per_unit))

  def calculate_current_loss(self, cur_price):
    if not self._has_lp_position or self._cur_lp_enter_price < 0.001:
      return
    
    lp_quit_base_amount = self._cur_lp_enter_base_amount * math.sqrt(self._cur_lp_enter_price / cur_price)
    lp_quit_quote_amount = self._cur_lp_enter_quote_amount * math.sqrt(cur_price / self._cur_lp_enter_price)
    raw_loss = (self._cur_lp_enter_base_amount * cur_price + self._cur_lp_enter_quote_amount) - (lp_quit_base_amount * cur_price + lp_quit_quote_amount)
    self._cur_lp_loss = raw_loss * self._klp_loss_adjust_factor * self._cur_lp_fee_loss_multiplier

  def calculate_current_fee(self, cur_price):
    if not self._has_lp_position or self._cur_lp_enter_price < 0.001:
      return
    
    if cur_price < self._cur_lp_price_upper and cur_price > self._cur_lp_price_lower:
      self._cur_lp_in_cnt += 1
      self._cur_lp_fee += self._klp_fee_per_unit * (self._cur_lp_enter_amount / 10000.0) * self._cur_lp_fee_loss_multiplier
    else:
      self._cur_lp_out_cnt += 1

  def calculate_current_accept_win_size(self, cur_price):
    if not self._has_lp_position:
      return None
    
    accept_loss = self._cur_lp_fee * self._kaccept_loss_factor
    accept_price_diff = math.sqrt(accept_loss / self._cur_loss_per_unit)
    return accept_price_diff

  def adjust_accept_win_size(self, accept_win_size, bias, cur_price):
    if not self._has_lp_position:
      return accept_win_size

    minus_factor = self._kminus_bias_adjust_bar - 0.5
    plus_factor = self._kplus_bias_adjust_bar - 0.5

    if bias > -(self._kminus_bias_adjust_bar ** 2) and bias < self._kplus_bias_adjust_bar ** 2:
      return accept_win_size
    
    if (cur_price - self._cur_lp_enter_price) * bias < 0.0:
      return accept_win_size

    bias_factor = ((math.sqrt(abs(bias)) - (minus_factor if bias < 0.0 else plus_factor)) * 2.0) ** 2
    accept_win_size *= bias_factor
    return accept_win_size

  def is_bias_real_withdraw(self, bias, cur_price):
    price_diff = cur_price - self._cur_lp_enter_price
    base_minus_bias_bar = self._kminus_bias_bar 
    base_plus_bias_bar = self._kplus_bias_bar
    half_win_size = self._kbias_bar_decay_max_size
    bias_bar_factor = 1.0
    if abs(price_diff) > self._kcan_adjust_bar + 0.001:
      bias_bar_factor = (half_win_size - abs(price_diff)) / (half_win_size - self._kcan_adjust_bar)
      bias_bar_factor = max(0.0, bias_bar_factor)
    
    if bias < 0.0:
      bias_bar = base_minus_bias_bar * bias_bar_factor
      logging.info("real bias bar:%f", bias_bar)
      return bias < -bias_bar
    else:
      bias_bar = base_plus_bias_bar * bias_bar_factor
      logging.info("real bias bar:%f", bias_bar)
      return bias > bias_bar

  def process_new_tick_bias(self, uniswap_ts, uniswap_price, binance_price):
    if uniswap_ts == self.last_processed_uniswap_ts:
      return
    self.last_processed_uniswap_ts = uniswap_ts
    self._mid_provide_bias_calculator.insert_record(binance_price, uniswap_price)
    self._long_provide_bias_calculator.insert_record(binance_price, uniswap_price)
    self._normal_withdraw_bias_calculator.insert_record(binance_price, uniswap_price)
    self._mid_provide_bias_calculator.print_info(self._logger)
    self._long_provide_bias_calculator.print_info(self._logger)
    self._normal_withdraw_bias_calculator.print_info(self._logger)

  def on_book_process(self, product, sub_req, book):
    if product.exchange == 'Uniswap':
      if abs(book.timestamp - self._last_binance_book_ts) <= self._kmax_diff_ms * 10 ** 6:
        self.process_new_tick_bias(book.timestamp, (book.ask0().price + book.bid0().price) / 2.0, (self._last_binance_ask0 + self._last_binance_bid0) / 2.0)

    elif product.exchange == 'Binance':
      if abs(book.timestamp - self._last_uniswap_book_ts) <= self._kmax_diff_ms * 10 ** 6:
        self.process_new_tick_bias(self._last_uniswap_book_ts, (self._last_uniswap_ask0 + self._last_uniswap_bid0) / 2.0, (book.ask0().price + book.bid0().price) / 2.0)

    if product != self._trade_product:
      return
    
    self._uniswap_tick_cnt += 1
    askp, bidp = book.ask0().price, book.bid0().price
    midp = (float)(askp + bidp) / 2.0
    self._logger.info('mid price:%f' % (midp))

    if not self._pos_initialized:
      self.initialize_pos(midp)
    
    self._window_estimator.insert_record(midp) 
    self._mid_win_estimator.insert_record(midp)

    if not self._mid_win_estimator.has_enough_record() or not self._normal_withdraw_bias_calculator.has_enough_record():
      return

    if self._uniswap_tick_cnt % 10 == 0:
      self.update_pos()

    if book.timestamp - self._last_op_ts < self._kmin_op_interval:
      return

    if not self._has_lp_position:
      if not self._window_estimator.has_enough_record()\
      or not self._mid_provide_bias_calculator.has_enough_record() or not self._long_provide_bias_calculator.has_enough_record():
        return

      avg_price = self._window_estimator.get_avg_price()
      half_window_size = self._window_estimator.get_half_window_size() * self._kwindow_size_multiplier
      if avg_price < 1.0e-6 or half_window_size < 1.0e-6:
        return
      
      if not self._mid_provide_bias_calculator.should_provide() or not self._long_provide_bias_calculator.should_provide() \
      or not self._normal_withdraw_bias_calculator.should_provide():
        return

      half_window_size = max(half_window_size, self._kmin_half_win_size)
      price_upper = midp + half_window_size
      price_lower = max(midp - half_window_size, 0.01)

      self._last_op_ts = book.timestamp
      self._logger.info('Order Sent[Provide liquidity]: %s, price_upper: %s, price_lower: %s' % (
        self._trade_product.symbol, price_upper, price_lower))
      self._executor.submit_provide_liquidity_order(
        action_type='PROVIDE',
        price_upper=price_upper,
        price_lower=price_lower)
      self._logger.info('Provide liquidity Submitted!!')
      self._op_provide_price = midp
      self._op_provide_price_upper = price_upper
      self._op_provide_price_lower = price_lower
      self._op_provide_amount = self._kdefault_amount
      self._op_provide_amount = self._executor.query_unbiased_position() * midp * 2.0
    else:
      self.calculate_current_fee(midp)
      self.calculate_current_loss(midp)
      if self._uniswap_tick_cnt % 1800 == 0:
        self.print_lp_info()
        self.write_status_to_file(self._cur_lp_enter_price, self._cur_lp_price_upper, self._cur_lp_price_lower, self._cur_lp_enter_amount, self._cur_lp_fee)
      accept_price_diff = self.calculate_current_accept_win_size(self._mid_win_estimator.get_avg_price())
      self._logger.info('accept price diff:%f' % (accept_price_diff))
      accept_price_diff = max(self._kcan_adjust_bar, accept_price_diff)
      accept_price_diff = self.adjust_accept_win_size(accept_price_diff, self._normal_withdraw_bias_calculator.get_bias_val(), midp)
      self._logger.info('adjusted accept price diff:%f' % (accept_price_diff))
      need_withdraw = self.is_bias_real_withdraw(self._normal_withdraw_bias_calculator.get_bias_val(), midp)

      if need_withdraw and abs(midp - self._cur_lp_enter_price) < accept_price_diff \
      or self._window_estimator.get_avg_price() < self._cur_lp_price_lower - self._kavg_price_out_of_range or self._window_estimator.get_avg_price() > self._cur_lp_price_upper + self._kavg_price_out_of_range:
        self._last_op_ts = book.timestamp
        self._logger.info('Order Sent[Withdraw liquidity]: %s' % (
            self._trade_product.symbol))
        self._executor.submit_withdraw_liquidity_order(
            action_type='WITHDRAW')
        self._logger.info('Withdraw liquidity Submitted!!')
        self._op_withdraw_price = midp

    return

class SimpleAMMGeneralSubStrategy(SingleProductSubStrategyAMMGeneralBase):
  def __init__(self, model_profile, dry_run, logger, strategy_logger, latency_recorder, parent):
    super().__init__(model_profile, dry_run, logger, strategy_logger, latency_recorder, parent) 
    self._last_op_ts = 0
    self._kmin_op_interval = int(self._config.get('min_op_interval', 40000000000))

    self._kwindow_estimator_normal_record_cnt = int(self._config.get('window_estimator_normal_record_cnt', 3600))
    self._kwindow_estimator_mid_record_cnt = int(self._config.get('window_estimator_mid_record_cnt', 60))
    self._kwindow_estimator_range_multiplier = float(self._config.get('window_estimator_range_multiplier', 2.0))
    
    self._kwindow_size_multiplier = float(self._config.get('window_size_multiplier', 3.0))

    self._kprovide_signal_bar = float(self._config.get('provide_signal_bar', 0.2 ** 2))
    self._knormal_withdraw_bias_record_cnt = int(self._config.get('normal_withdraw_bias_record_cnt', 60))
    self._kmid_provide_bias_record_cnt = int(self._config.get('mid_provide_bias_record_cnt', 600))
    self._klong_provide_bias_record_cnt = int(self._config.get('long_provide_bias_record_cnt', 3600))

    self._window_estimator = WindowEstimator(max_record_cnt=self._kwindow_estimator_normal_record_cnt, range_multiplier=self._kwindow_estimator_range_multiplier)
    self._mid_win_estimator = WindowEstimator(max_record_cnt=self._kwindow_estimator_mid_record_cnt, range_multiplier=self._kwindow_estimator_range_multiplier)
    self._normal_withdraw_bias_calculator = BiasCalculator(max_record_cnt=self._knormal_withdraw_bias_record_cnt, withdraw_signal_bar=1.2 ** 2, provide_signal_bar=self._kprovide_signal_bar)
    self._mid_provide_bias_calculator = BiasCalculator(max_record_cnt=self._kmid_provide_bias_record_cnt, withdraw_signal_bar=2.5 ** 2, provide_signal_bar=self._kprovide_signal_bar)
    self._long_provide_bias_calculator = BiasCalculator(max_record_cnt=self._klong_provide_bias_record_cnt, withdraw_signal_bar=2.5 ** 2, provide_signal_bar=self._kprovide_signal_bar)

    self._kmin_half_win_size = float(self._config.get('min_half_win_size', 75.0))
    self._kcan_adjust_bar = float(self._config.get('can_adjust_bar', 5.0))
    self._kaccept_loss_factor = float(self._config.get('accept_loss_factor', 0.8))
    self._kminus_bias_bar = float(self._config.get('minus_bias_bar', 1.2 ** 2))
    self._kplus_bias_bar = float(self._config.get('plus_bias_bar', 1.1 ** 2))
    self._kbias_bar_decay_max_size = float(self._config.get('bias_bar_decay_max_size', 30.0))
    self._kavg_price_out_of_range = float(self._config.get('avg_price_out_of_range', 10.0))

    self._kstatus_record_file_path = str(self._config.get('status_record_file_path', os.path.expanduser('~/simple_amm_status')))

    self._klp_fee_per_unit = 0.000381944 / 2.0
    self._klp_loss_adjust_factor = (0.26 / 0.001924) / 2.0
    self._has_lp_position = False
    self._cur_lp_enter_price = 0.0
    self._cur_lp_enter_amount = 0.0
    self._cur_lp_enter_base_amount = 0.0
    self._cur_lp_enter_quote_amount = 0.0
    self._cur_lp_price_lower = 0.0
    self._cur_lp_price_upper = 0.0
    self._cur_lp_fee_loss_multiplier = 0.0
    self._cur_lp_in_cnt = 0
    self._cur_lp_out_cnt = 0

    self._cur_loss_per_unit = 0.0
    self._cur_lp_loss = 0.0
    self._total_lp_loss = 0.0

    self._cur_lp_fee = 0.0
    self._total_fee = 0.0
    self._total_net_profit = 0.0
    self._last_lp_out_percentage = 0.0

    self.last_processed_uniswap_ts = 0

    self._uniswap_tick_cnt = 0

    self._pos_initialized = False
    self._is_updating_pos = False
    self._kdefault_amount = 10000.0
    self._op_provide_price = 0.0
    self._op_provide_price_upper = 0.0
    self._op_provide_price_lower = 0.0
    self._op_provide_amount = 0.0
    self._op_withdraw_price = 0.0

  def reset_lp_data(self):
    self._has_lp_position = False
    self._cur_lp_enter_price = 0.0
    self._cur_lp_enter_amount = 0.0
    self._cur_lp_enter_base_amount = 0.0
    self._cur_lp_enter_quote_amount = 0.0
    self._cur_lp_price_lower = 0.0
    self._cur_lp_price_upper = 0.0
    self._cur_lp_fee_loss_multiplier = 0.0
    self._cur_lp_in_cnt = 0
    self._cur_lp_out_cnt = 0
    self._cur_lp_loss = 0.0
    self._cur_lp_fee = 0.0
    self._cur_loss_per_unit = 0.0

  def print_lp_info(self):
    if not self._has_lp_position:
      return
    
    self._logger.info('current liquidity: enter price:%f, amount: %f, price lower:%f, price upper:%f, record in:%d, record out:%f' % (
            self._cur_lp_enter_price, self._cur_lp_enter_amount, self._cur_lp_price_lower, self._cur_lp_price_upper, self._cur_lp_in_cnt, self._cur_lp_out_cnt))
    self._logger.info('current status: current loss:%f, current fee: %f, total loss:%f, total fee:%f, total net profit:%f' % (
      self._cur_lp_loss, self._cur_lp_fee, self._total_lp_loss, self._total_fee, self._total_net_profit))

  def write_status_to_file(self, cur_price, price_upper, price_lower, amount):
    try:
      with open(self._kstatus_record_file_path, "w") as status_file:
        if not(cur_price is None or price_upper is None or price_lower is None or amount is None):
          status_msg = '%f,%f,%f,%f' % (cur_price, price_upper, price_lower, amount)
          status_file.write(status_msg)
    except Exception:
        self._logger.exception('Exception during reading status file.')

  def read_status_from_file(self, default_price):
    cur_price = default_price
    price_upper = cur_price + self._kmin_half_win_size
    price_lower = max(0.01, cur_price - self._kmin_half_win_size)
    amount = self._kdefault_amount
    try:
      with open(self._kstatus_record_file_path, "r") as status_file:
        line = status_file.readline()
        linepart = line.split(',')
        if len(linepart) >= 4:
          cur_price = float(linepart[0])
          price_upper = float(linepart[1])
          price_lower = float(linepart[2])
          amount = float(linepart[3])
    except Exception:
        self._logger.exception('Exception during reading status file.')
    return (cur_price, price_upper, price_lower, amount)

  def initialize_pos(self, price):
    has_nft_token = self._executor.is_holding_nft_token(symbol=self._trade_product.symbol)
    if has_nft_token and not self._pos_initialized and not self._is_updating_pos:
      self._is_updating_pos = True
      self._logger.info("Has position when strategy starts, updating.")
      (cur_price, price_upper, price_lower, amount) = self.read_status_from_file(price)
      self.update_lp_data_after_provide(cur_price, price_upper, price_lower, amount)
      self._is_updating_pos = False
    self._pos_initialized = True

  def update_pos(self):
    has_nft_token = self._executor.is_holding_nft_token(symbol=self._trade_product.symbol)
    if self._has_lp_position != has_nft_token and not self._is_updating_pos:
      self._is_updating_pos = True
      if self._has_lp_position and self._op_withdraw_price > 0.001:
        self.update_lp_data_after_withdraw(self._op_withdraw_price)
        self.write_status_to_file(None, None, None, None)
        self._op_withdraw_price = 0.0
      elif not self._has_lp_position and self._op_provide_price > 0.001:
        self.update_lp_data_after_provide(self._op_provide_price, self._op_provide_price_upper, self._op_provide_price_lower, self._op_provide_amount)
        self.write_status_to_file(self._op_provide_price, self._op_provide_price_upper, self._op_provide_price_lower, self._op_provide_amount)
        self._op_provide_price = 0.0
        self._op_provide_price_upper = 0.0
        self._op_provide_price_lower = 0.0
        self._op_provide_amount = 0.0
      else:
        self._has_lp_position = not self._has_lp_position
      self._is_updating_pos = False

  def update_lp_data_after_withdraw(self, cur_price):
    if not self._has_lp_position:
      return
    
    self._total_lp_loss += self._cur_lp_loss
    self._total_fee += self._cur_lp_fee
    self._total_net_profit = self._total_fee - self._total_lp_loss
    if self._cur_lp_out_cnt + self._cur_lp_in_cnt == 0:
      self._last_lp_out_percentage = -1
    else:
      self._last_lp_out_percentage = (float)(self._cur_lp_out_cnt) / (float)(self._cur_lp_out_cnt + self._cur_lp_in_cnt)
    self._logger.info('withdraw liquidity: enter price:%f, quit price: %f, amount: %f, price lower:%f, price upper:%f, record span:%d, record out percentage:%f' % (
            self._cur_lp_enter_price, cur_price, self._cur_lp_enter_amount, self._cur_lp_price_lower, self._cur_lp_price_upper, self._cur_lp_in_cnt + self._cur_lp_out_cnt, self._last_lp_out_percentage))
    self._logger.info('current status: current loss:%f, current fee: %f, total loss:%f, total fee:%f, total net profit:%f' % (
      self._cur_lp_loss, self._cur_lp_fee, self._total_lp_loss, self._total_fee, self._total_net_profit))
    self.reset_lp_data()

  def update_lp_data_after_provide(self, cur_price, price_upper, price_lower, amount):
    if self._has_lp_position:
      return

    self._has_lp_position = True
    self._cur_lp_enter_price = cur_price
    self._cur_lp_enter_amount = amount
    self._cur_lp_enter_base_amount = amount / (cur_price * 2.0) 
    self._cur_lp_enter_quote_amount = amount / 2.0
    self._cur_lp_price_lower = price_lower
    self._cur_lp_price_upper = price_upper
    self._cur_lp_fee_loss_multiplier = 100.0 / (price_upper - price_lower)
    self._cur_lp_in_cnt = 0
    self._cur_lp_out_cnt = 0
    self._cur_lp_loss = 0.0
    self._cur_lp_fee = 0.0
    self.calculate_current_loss(cur_price + 1.0)
    self._cur_loss_per_unit = self._cur_lp_loss
    self._logger.info('provide liquidity: enter price:%f, amount: %f, price lower:%f, price upper:%f, loss per unit:%f' % (
      self._cur_lp_enter_price, self._cur_lp_enter_amount, self._cur_lp_price_lower, self._cur_lp_price_upper, self._cur_loss_per_unit))

  def calculate_current_loss(self, cur_price):
    if not self._has_lp_position or self._cur_lp_enter_price < 0.001:
      return
    
    lp_quit_base_amount = self._cur_lp_enter_base_amount * math.sqrt(self._cur_lp_enter_price / cur_price)
    lp_quit_quote_amount = self._cur_lp_enter_quote_amount * math.sqrt(cur_price / self._cur_lp_enter_price)
    raw_loss = (self._cur_lp_enter_base_amount * cur_price + self._cur_lp_enter_quote_amount) - (lp_quit_base_amount * cur_price + lp_quit_quote_amount)
    self._cur_lp_loss = raw_loss * self._klp_loss_adjust_factor * self._cur_lp_fee_loss_multiplier

  def calculate_current_fee(self, cur_price):
    if not self._has_lp_position or self._cur_lp_enter_price < 0.001:
      return
    
    if cur_price < self._cur_lp_price_upper and cur_price > self._cur_lp_price_lower:
      self._cur_lp_in_cnt += 1
      self._cur_lp_fee += self._klp_fee_per_unit * (self._cur_lp_enter_amount / 10000.0) * self._cur_lp_fee_loss_multiplier
    else:
      self._cur_lp_out_cnt += 1

  def calculate_current_accept_win_size(self, cur_price):
    if not self._has_lp_position:
      return None
    
    accept_loss = self._cur_lp_fee * self._kaccept_loss_factor
    accept_price_diff = math.sqrt(accept_loss / self._cur_loss_per_unit)
    return accept_price_diff

  def is_bias_real_withdraw(self, bias, cur_price):
    price_diff = cur_price - self._cur_lp_enter_price
    base_minus_bias_bar = self._kminus_bias_bar 
    base_plus_bias_bar = self._kplus_bias_bar
    half_win_size = self._kbias_bar_decay_max_size
    bias_bar_factor = 1.0
    if abs(price_diff) > self._kcan_adjust_bar + 0.001:
      bias_bar_factor = (half_win_size - abs(price_diff)) / (half_win_size - self._kcan_adjust_bar)
      bias_bar_factor = max(0.0, bias_bar_factor)
    
    if bias < 0.0:
      bias_bar = base_minus_bias_bar * bias_bar_factor
      logging.info("real bias bar:%f", bias_bar)
      return bias < -bias_bar
    else:
      bias_bar = base_plus_bias_bar * bias_bar_factor
      logging.info("real bias bar:%f", bias_bar)
      return bias > bias_bar

  def process_new_tick_bias(self, uniswap_ts, uniswap_price, binance_price):
    if uniswap_ts == self.last_processed_uniswap_ts:
      return
    self.last_processed_uniswap_ts = uniswap_ts
    self._mid_provide_bias_calculator.insert_record(binance_price, uniswap_price)
    self._long_provide_bias_calculator.insert_record(binance_price, uniswap_price)
    self._normal_withdraw_bias_calculator.insert_record(binance_price, uniswap_price)
    self._mid_provide_bias_calculator.print_info(self._logger)
    self._long_provide_bias_calculator.print_info(self._logger)
    self._normal_withdraw_bias_calculator.print_info(self._logger)

  def on_book_process(self, product, sub_req, book):
    if product != self._trade_product:
      return
    
    self._uniswap_tick_cnt += 1
    askp, bidp = book.ask0().price, book.bid0().price
    midp = (float)(askp + bidp) / 2.0
    self._logger.info('mid price:%f' % (midp))

    if not self._pos_initialized:
      self.initialize_pos(midp)

    if self._uniswap_tick_cnt < 5:
      return

    mid_usdc_usdt = (self._last_book_dict['Binance']['USDC-USDT'].ask0 + self._last_book_dict['Binance']['USDC-USDT'].bid0) / 2.0
    binance_ask0 = self._last_book_dict['Binance']['ETH-USDT'].ask0 / mid_usdc_usdt
    binance_bid0 = self._last_book_dict['Binance']['ETH-USDT'].bid0 / mid_usdc_usdt
    self._logger.info("binance calculate: ask0:%f, bid0:%f." % (binance_ask0, binance_bid0))

    self.process_new_tick_bias(book.timestamp, midp, (binance_ask0 + binance_bid0) / 2.0)
    self._window_estimator.insert_record(midp) 
    self._mid_win_estimator.insert_record(midp)

    if not self._mid_win_estimator.has_enough_record() or not self._normal_withdraw_bias_calculator.has_enough_record():
      return

    if self._uniswap_tick_cnt % 10 == 0:
      self.update_pos()

    if book.timestamp - self._last_op_ts < self._kmin_op_interval:
      return

    if not self._has_lp_position:
      if not self._window_estimator.has_enough_record()\
      or not self._mid_provide_bias_calculator.has_enough_record() or not self._long_provide_bias_calculator.has_enough_record():
        return

      avg_price = self._window_estimator.get_avg_price()
      half_window_size = self._window_estimator.get_half_window_size() * self._kwindow_size_multiplier
      if avg_price < 1.0e-6 or half_window_size < 1.0e-6:
        return
      
      if not self._mid_provide_bias_calculator.should_provide() or not self._long_provide_bias_calculator.should_provide() \
      or not self._normal_withdraw_bias_calculator.should_provide():
        return

      half_window_size = max(half_window_size, self._kmin_half_win_size)
      price_upper = midp + half_window_size
      price_lower = max(midp - half_window_size, 0.01)

      self._last_op_ts = book.timestamp
      self._logger.info('Order Sent[Provide liquidity]: %s, price_upper: %s, price_lower: %s' % (
        self._trade_product.symbol, price_upper, price_lower))
      self._executor.submit_provide_liquidity_order(
        action_type='PROVIDE',
        price_upper=price_upper,
        price_lower=price_lower)
      self._logger.info('Provide liquidity Submitted!!')
      self._op_provide_price = midp
      self._op_provide_price_upper = price_upper
      self._op_provide_price_lower = price_lower
      self._op_provide_amount = self._kdefault_amount
      self._op_provide_amount = self._executor.query_unbiased_position() * midp * 2.0
    else:
      self.calculate_current_fee(midp)
      self.calculate_current_loss(midp)
      if self._uniswap_tick_cnt % 1800 == 0:
        self.print_lp_info()
      accept_price_diff = self.calculate_current_accept_win_size(self._mid_win_estimator.get_avg_price())
      self._logger.info('accept price diff:%f' % (accept_price_diff))
      accept_price_diff = max(self._kcan_adjust_bar, accept_price_diff)
      need_withdraw = self.is_bias_real_withdraw(self._normal_withdraw_bias_calculator.get_bias_val(), midp)

      if need_withdraw and abs(midp - self._cur_lp_enter_price) < accept_price_diff \
      or self._window_estimator.get_avg_price() < self._cur_lp_price_lower - self._kavg_price_out_of_range or self._window_estimator.get_avg_price() > self._cur_lp_price_upper + self._kavg_price_out_of_range:
        self._last_op_ts = book.timestamp
        self._logger.info('Order Sent[Withdraw liquidity]: %s' % (
            self._trade_product.symbol))
        self._executor.submit_withdraw_liquidity_order(
            action_type='WITHDRAW')
        self._logger.info('Withdraw liquidity Submitted!!')
        self._op_withdraw_price = midp

    return

class SimpleArbStrategy(UnaryMultifeedStrategyBase):
  def __init__(self, strat_config, dry_run=True, exit_after_min=None, logger=None):
    self._logger = logger or logging.getLogger('SimpleArbStrategy')
    assert isinstance(strat_config, PassUnhedgeStrategyConfig)
    self._config = strat_config
    self._set_traits(self._config.trade_subreq)
    self._exit_after_min = exit_after_min
    super().__init__(products=self._config.get_products(),
                     currencies=self._config.get_currencies(),
                     logger=self._logger)

    self._book = BookMap(self._products, logger=self._logger)

    self._reserve_map = self._gen_reserve_map()
    printer_cls = get_pnl_balance_printer_cls(self._config.trade_subreq)
    self._printer = printer_cls(self._products, self._book, self._reserve_map, logger=self._logger)

    self._fill_dumper = FillLogDumper(self._config.trade_subreq)

    self._order_canceled_after_og_ready = True
    self._exit_attempt = 0
    self._exit_executor = None
    self._strategy_logger = StrategyLogProvider(
        self._config.strategy_request, log_root=os.path.expanduser('~/data/strat_slim_proto_log'))
    self._strategy_logger.write_request()
    if strat_config.config['common'].get('enable_telemetry'):
      self._latency_recorder = LatencyRecorder('life_of_signal', self._strategy_logger)
      self._logger.info('telemetry enabled!')
    else:
      self._latency_recorder = None
      self._logger.info('telemetry disabled!')

    self._single_product_sub_strategies = []
    for profile in self._config.model_profile.values():
      self._single_product_sub_strategies.append(
          SimpleAMMSubStrategy(model_profile=profile,
                                   dry_run=dry_run,
                                   logger=self._logger,
                                   strategy_logger=self._strategy_logger,
                                   latency_recorder=self._latency_recorder,
                                   parent=self))

  @property
  def config(self):
    return self._config

  @property
  def strategy_logger(self):
    return self._strategy_logger

  def _gen_reserve_map(self):
    reserve_map = {}
    if self._config.trade_subreq.market_type == 'Spot':
      for profile in self._config.model_profile.values():
        currency = profile.trade_product.base.currency
        min_pos = profile.config_dict['min_pos']
        max_pos = profile.config_dict['max_pos']
        reserve = 0.5 * (min_pos + max_pos)
        reserve_map[currency] = reserve
        reserve_map[profile.trade_product.quote.currency] = 0
    return reserve_map

  def dump_pnl_balance(self):
    self._printer.print_pnl_balance()
    pnl_balances = self._printer.calculate_pnl_balance()
    if pnl_balances is not None:
      for pnl_balance in pnl_balances:
        self._strategy_logger.write_pnl_balance(pnl_balance=pnl_balance, quote=None, subreq_id=None)
    if self._reserve_map:
      self._strategy_logger.write_reserve(
        reserve_as_proto(self._config.trade_subreq, self._reserve_map),
        account_request=self.order_gateway.og_logger.request)

  def prepare(self):
    for profile in self.config.model_profile.values():
      product = profile.trade_product
      feed_sub_request = FeedSubscriptionRequest.create(to_list(product), profile.trade_subreq)
      self.feed_subsystem.subscribe_book(feed_sub_request, functools.partial(self.on_book, product))
    for strat in self._single_product_sub_strategies:
      strat.prepare(self.feed_subsystem)
    # Start dumping pnl balance every 30 seconds
    PeriodicCallback(self.dump_pnl_balance, 30 * 1000).start()
    if self._exit_after_min is not None:
      self.register_exit(datetime.timedelta(minutes=self._exit_after_min))

  def on_og_reset(self):
    if not self._order_canceled_after_og_ready:
      self.order_gateway.cancel_all()
      self._logger.info('Orders canceled after OGs are ready.')
      self._order_canceled_after_og_ready = True

    self._printer.register_order_gateway(self.order_gateway)
    for strat in self._single_product_sub_strategies:
      strat.on_og_reset(order_gateway=self.order_gateway)
    self.order_gateway.register_fill_callback(self._fill_dumper.dump_fill)

    self._exit_executor = SimpleCancelAllExecutor()

    if self._reserve_map:
      self._strategy_logger.write_reserve(reserve_as_proto(self._config.trade_subreq,
                                                           self._reserve_map),
                                          account_request=self.order_gateway.og_logger.request)

    # TODO(chensili): Add OkexFutures limit error check

  def on_book(self, product, book):
    self._book.set_book(product, book)

  def signal_handler(self, sig, frame):
    self._logger.warning('Caught signal: %s', sig)
    self._ioloop.add_callback_from_signal(self.on_exit)

  def register_exit(self, timedelta=None):
    if timedelta is None:
      self._ioloop.add_callback(self.on_exit)
    else:
      self._ioloop.add_timeout(timedelta, self.on_exit)

  def on_exit(self):
    for strat in self._single_product_sub_strategies:
      strat.set_exit()

    self._exit_attempt += 1
    if self._exit_attempt > 5:
      self._ioloop.add_callback(self._ioloop.stop)

    finished = True
    if self._exit_executor is not None:
      ogs = to_list(self.order_gateway)
      self._logger.info('Exit callback executed.')
      finished = self._exit_executor.cancel_all_on_exit(ogs)

    if finished:
      self._ioloop.add_callback(self._ioloop.stop)
    else:
      timeout = datetime.timedelta(seconds=2)
      self._ioloop.add_timeout(timeout, self.on_exit)

def main(argv):
  init_og_after_min = 2

  strat_config = PassUnhedgeStrategyConfig(load_clean_json(FLAGS.config_filename))
  strategy = SimpleArbStrategy(strat_config,
                               dry_run=FLAGS.dry_run,
                               exit_after_min=FLAGS.exit_after_min)
  signal.signal(signal.SIGTERM, strategy.signal_handler)
  signal.signal(signal.SIGINT, strategy.signal_handler)

  feed_checker_config = strat_config.feed_checker_config
  og_config_dict = {'owner': FLAGS.og_config_name}  # wallet address
  return strategy.run_live(strategy=strategy,
                           on_order_gateway_reset_callback=strategy.on_og_reset,
                           og_config_name=None,
                           og_params=strategy.config.og_params,
                           init_order_gateway_after=datetime.timedelta(seconds=init_og_after_min),
                           feed_checker_config=feed_checker_config,
                           og_log_callback=strategy.strategy_logger.write_og_log,
                           slow_callback_duration=0.01,
                           og_config_dict=og_config_dict)


if __name__ == '__main__':
  flags.DEFINE_string('og_config_name', None, 'og_config_name')

  flags.DEFINE_string('config_filename', '', 'config_filename')

  flags.DEFINE_integer('exit_after_min', 1440, 'exit_after_min')

  flags.DEFINE_boolean('dry_run', False, 'dry_run')

  flags.DEFINE_float('slow_callback_duration', None, 'slow_callback_duration')

  logging.basicConfig(level='INFO', format='%(levelname)s %(asctime)s %(name)s] %(message)s')
  app.run(main)
